/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.taketoday.context.cglib.beans;

import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import cn.taketoday.context.asm.ClassVisitor;
import cn.taketoday.context.cglib.core.AbstractClassGenerator;
import cn.taketoday.context.cglib.core.CglibReflectUtils;
import cn.taketoday.context.cglib.core.KeyFactory;
import cn.taketoday.context.utils.Assert;

/**
 * A <code>Map</code>-based view of a JavaBean. The default set of keys is the
 * union of all property names (getters or setters). An attempt to set a
 * read-only property will be ignored, and write-only properties will be
 * returned as <code>null</code>. Removal of objects is not a supported (the key
 * set is fixed).
 *
 * @author Chris Nokleberg
 */
@SuppressWarnings("all")
abstract public class BeanMap implements Map {
  /**
   * Limit the properties reflected in the key set of the map to readable
   * properties.
   *
   * @see BeanMap.Generator#setRequire
   */
  public static final int REQUIRE_GETTER = 1;

  /**
   * Limit the properties reflected in the key set of the map to writable
   * properties.
   *
   * @see BeanMap.Generator#setRequire
   */
  public static final int REQUIRE_SETTER = 2;

  /**
   * Helper method to create a new <code>BeanMap</code>. For finer control over
   * the generated instance, use a new instance of <code>BeanMap.Generator</code>
   * instead of this static method.
   *
   * @param bean
   *         the JavaBean underlying the map
   *
   * @return a new <code>BeanMap</code> instance
   */
  public static BeanMap create(Object bean) {
    Generator gen = new Generator();
    gen.setBean(bean);
    return gen.create();
  }

  public static class Generator extends AbstractClassGenerator {

    private static final BeanMapKey KEY_FACTORY = KeyFactory.create(BeanMapKey.class, KeyFactory.CLASS_BY_NAME);

    interface BeanMapKey {
      public Object newInstance(Class type, int require);
    }

    private Object bean;
    private Class beanClass;
    private int require;

    public Generator() {
      super(BeanMap.class);
    }

    /**
     * Set the bean that the generated map should reflect. The bean may be swapped
     * out for another bean of the same type using {@link #setBean}. Calling this
     * method overrides any value previously set using {@link #setBeanClass}. You
     * must call either this method or {@link #setBeanClass} before {@link #create}.
     *
     * @param bean
     *         the initial bean
     */
    public void setBean(Object bean) {
      this.bean = bean;
      if (bean != null) beanClass = bean.getClass();
    }

    /**
     * Set the class of the bean that the generated map should support. You must
     * call either this method or {@link #setBeanClass} before {@link #create}.
     *
     * @param beanClass
     *         the class of the bean
     */
    public void setBeanClass(Class beanClass) {
      this.beanClass = beanClass;
    }

    /**
     * Limit the properties reflected by the generated map.
     *
     * @param require
     *         any combination of {@link #REQUIRE_GETTER} and
     *         {@link #REQUIRE_SETTER}; default is zero (any property allowed)
     */
    public void setRequire(int require) {
      this.require = require;
    }

    protected ClassLoader getDefaultClassLoader() {
      return beanClass.getClassLoader();
    }

    protected ProtectionDomain getProtectionDomain() {
      return CglibReflectUtils.getProtectionDomain(beanClass);
    }

    /**
     * Create a new instance of the <code>BeanMap</code>. An existing generated
     * class will be reused if possible.
     */
    public BeanMap create() {
      Assert.notNull(beanClass, "Class of bean unknown");
      setNamePrefix(beanClass.getName());
      return (BeanMap) super.create(KEY_FACTORY.newInstance(beanClass, require));
    }

    public void generateClass(ClassVisitor v) throws Exception {
      new BeanMapEmitter(v, getClassName(), beanClass, require);
    }

    protected Object firstInstance(Class type) {
      return ((BeanMap) CglibReflectUtils.newInstance(type)).newInstance(bean);
    }

    protected Object nextInstance(Object instance) {
      return ((BeanMap) instance).newInstance(bean);
    }
  }

  /**
   * Create a new <code>BeanMap</code> instance using the specified bean. This is
   * faster than using the {@link #create} static method.
   *
   * @param bean
   *         the JavaBean underlying the map
   *
   * @return a new <code>BeanMap</code> instance
   */
  abstract public BeanMap newInstance(Object bean);

  /**
   * Get the type of a property.
   *
   * @param name
   *         the name of the JavaBean property
   *
   * @return the type of the property, or null if the property does not exist
   */
  abstract public Class getPropertyType(String name);

  protected Object bean;

  protected BeanMap() {}

  protected BeanMap(Object bean) {
    setBean(bean);
  }

  public Object get(Object key) {
    return get(bean, key);
  }

  public Object put(Object key, Object value) {
    return put(bean, key, value);
  }

  /**
   * Get the property of a bean. This allows a <code>BeanMap</code> to be used
   * statically for multiple beans--the bean instance tied to the map is ignored
   * and the bean passed to this method is used instead.
   *
   * @param bean
   *         the bean to query; must be compatible with the type of this
   *         <code>BeanMap</code>
   * @param key
   *         must be a String
   *
   * @return the current value, or null if there is no matching property
   */
  abstract public Object get(Object bean, Object key);

  /**
   * Set the property of a bean. This allows a <code>BeanMap</code> to be used
   * statically for multiple beans--the bean instance tied to the map is ignored
   * and the bean passed to this method is used instead.
   *
   * @param key
   *         must be a String
   *
   * @return the old value, if there was one, or null
   */
  abstract public Object put(Object bean, Object key, Object value);

  /**
   * Change the underlying bean this map should use.
   *
   * @param bean
   *         the new JavaBean
   *
   * @see #getBean
   */
  public void setBean(Object bean) {
    this.bean = bean;
  }

  /**
   * Return the bean currently in use by this map.
   *
   * @return the current JavaBean
   *
   * @see #setBean
   */
  public Object getBean() {
    return bean;
  }

  public void clear() {
    throw new UnsupportedOperationException();
  }

  public boolean containsKey(Object key) {
    return keySet().contains(key);
  }

  public boolean containsValue(final Object value) {
    for (final Object key : keySet()) {
      final Object v = get(key);
      if (Objects.equals(value, v)) {
        return true;
      }
    }
    return false;
  }

  public int size() {
    return keySet().size();
  }

  public boolean isEmpty() {
    return size() == 0;
  }

  public Object remove(Object key) {
    throw new UnsupportedOperationException();
  }

  public void putAll(Map t) {

    for (final Object key : t.keySet()) {
      put(key, t.get(key));
    }
  }

  public boolean equals(Object o) {
    if (o != this) {
      if (!(o instanceof Map)) {
        return false;
      }
      Map other = (Map) o;
      if (size() != other.size()) {
        return false;
      }
      for (final Object key : keySet()) {
        if (!Objects.equals(get(key), other.get(key))) {
          return false;
        }
      }
    }
    return true;
  }

  public int hashCode() {
    int code = 0;
    for (final Object key : keySet()) {
      code += Objects.hashCode(key) ^ Objects.hashCode(get(key));
    }
    return code;
  }

  // TODO: optimize
  public Set entrySet() {
    HashMap copy = new HashMap();
    for (final Object key : keySet()) {
      copy.put(key, get(key));
    }
    return Collections.unmodifiableSet(copy.entrySet());
  }

  public Collection values() {
    Set keys = keySet();
    ArrayList values = new ArrayList(keys.size());
    for (Object key : keys) {
      values.add(get(key));
    }
    return Collections.unmodifiableCollection(values);
  }

  /*
   * @see java.util.AbstractMap#toString
   */
  public String toString() {
    StringBuffer sb = new StringBuffer();
    sb.append('{');
    for (Iterator it = keySet().iterator(); it.hasNext(); ) {
      Object key = it.next();
      sb.append(key);
      sb.append('=');
      sb.append(get(key));
      if (it.hasNext()) {
        sb.append(", ");
      }
    }
    sb.append('}');
    return sb.toString();
  }
}
